/**
 * Author: balance
 * Datetime: 2010-9-6
 * Note:
 */
package com.ruoyi.framework.shiro.util.secret;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Algorithm {
    private static final Logger log = LoggerFactory.getLogger(Algorithm.class);

    /**
     * 公钥key
     */
    public static final String PUBLIC_KEY = "PublicKey";
    /**
     * 私钥key
     */
    public static final String PRIVATE_KEY = "PrivateKey";

    /**
     * AES-CBC-128 Note: 补码方式-PKCS5Padding; IV向量：0102030405060708
     * 当明文大于16字节，加密后32字节； 当明文小雨16字节，加密后16字节；
     */
    public static byte[] aesEncrypt(String original, byte[] key, byte[] ivBytes) throws Exception {
        if (key == null || key.length != 16) {
            return null;
        }

        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //NOSONAR  sonar扫描忽略这段代码 算法、模式、补码方式
        IvParameterSpec iv = new IvParameterSpec(ivBytes);//NOSONAR  sonar扫描忽略这段代码 使用CBC模式，需要一个向量iv，可增加加密算法的强度
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

        return cipher.doFinal(original.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * @param original
     * @param key
     * @param ivBytes
     * @return note: AES-CBC-128 明文为Byte[]类型 pkcs5
     * @throws Exception
     */
    public static byte[] aesEncrypt(byte[] original, byte[] key, byte[] ivBytes) throws Exception {
        if (key == null || key.length != 16) {
            return null;
        }

        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");//NOSONAR  sonar扫描忽略这段代码
        IvParameterSpec iv = new IvParameterSpec(ivBytes);//NOSONAR  sonar扫描忽略这段代码
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        byte[] encrypted = cipher.doFinal(original);
        return encrypted;
    }

    /**
     * @param original
     * @param key
     * @param ivBytes
     * @return note: AES-CBC-128 明文为Byte[]类型 pkcs0
     * @throws Exception
     */
    public static byte[] aesEncryptnopadding(byte[] original, byte[] key, byte[] ivBytes)
            throws Exception {
        if (key == null || key.length != 16) {
            return null;
        }

        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/noPadding");//NOSONAR  sonar扫描忽略这段代码
        IvParameterSpec iv = new IvParameterSpec(ivBytes);//NOSONAR  sonar扫描忽略这段代码
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        byte[] encrypted = cipher.doFinal(original);
        return encrypted;
    }

    /**
     * @param encrypted
     * @param key
     * @param ivBytes
     * @return
     * @throws Exception
     * @note: AES-CBC-128 PKCS5
     */
    public static String aesDecrypt(byte[] encrypted, byte[] key, byte[] ivBytes) throws Exception {
        if (key == null || key.length != 16) {
            return null;
        }

        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //NOSONAR  sonar扫描忽略这段代码 算法、模式、补码方式
        IvParameterSpec iv = new IvParameterSpec(ivBytes);//NOSONAR  sonar扫描忽略这段代码 使用CBC模式，需要一个向量iv，可增加加密算法的强度
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);//NOSONAR  sonar扫描忽略这段代码
        byte[] original = cipher.doFinal(encrypted);

        return new String(original, StandardCharsets.UTF_8);// mod by hc 20121115
    }

    /**
     * @param encrypted
     * @param key
     * @param ivBytes
     * @return
     * @throws Exception
     * @note: AES-CBC-128 PKCS5
     */
    public static byte[] aesDecryptByte(byte[] encrypted, byte[] key, byte[] ivBytes)
            throws Exception {
        if (key == null || key.length != 16) {
            return null;
        }

        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //NOSONAR  sonar 算法、模式、补码方式
        IvParameterSpec iv = new IvParameterSpec(ivBytes);//NOSONAR  sonar扫描忽略这段代码 使用CBC模式，需要一个向量iv，可增加加密算法的强度
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
        byte[] original = cipher.doFinal(encrypted);

        return original;
    }

    /**
     * @param encrypted
     * @param key
     * @param ivBytes
     * @return
     * @throws Exception
     * @note: AES-CBC-128 PKCS0
     */
    public static String aesDecryptnopadding(byte[] encrypted, byte[] key, byte[] ivBytes)
            throws Exception {
        if (key == null || key.length != 16) {
            return null;
        }

        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/noPadding");//NOSONAR  sonar扫描忽略这段代码 算法、模式、补码方式
        IvParameterSpec iv = new IvParameterSpec(ivBytes);//NOSONAR  sonar扫描忽略这段代码 使用CBC模式，需要一个向量iv，可增加加密算法的强度
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
        byte[] original = cipher.doFinal(encrypted);

        // remove padding
        int pad = (int) original[original.length - 1];
        boolean isPadding = false;
        if (pad > 0 && pad <= 15) {
            isPadding = true;
            for (int i = original.length - 1; i >= original.length - pad; i--) {
                if (pad != Integer.valueOf(original[i])) {
                    isPadding = false;
                    break;
                }
            }
        }
        if (isPadding) {
            original = Arrays.copyOfRange(original, 0, original.length - pad);
        }

        return new String(original, StandardCharsets.UTF_8);
    }

    /**
     * @param encrypted
     * @param key
     * @param ivBytes
     * @return
     * @throws Exception
     * @note: AES-CBC-128 PKCS0
     */
    public static byte[] aesDecryptnopaddingByte(byte[] encrypted, byte[] key, byte[] ivBytes)
            throws Exception {
        if (key == null || key.length != 16) {
            return null;
        }

        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/noPadding");//NOSONAR  sonar扫描忽略这段代码 算法、模式、补码方式
        IvParameterSpec iv = new IvParameterSpec(ivBytes);//NOSONAR  sonar扫描忽略这段代码 使用CBC模式，需要一个向量iv，可增加加密算法的强度
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
        byte[] original = cipher.doFinal(encrypted);

        // remove padding
        int pad = (int) original[original.length - 1];
        boolean isPadding = false;
        if (pad > 0 && pad <= 15) {
            isPadding = true;
            for (int i = original.length - 1; i >= original.length - pad; i--) {
                if (pad != Integer.valueOf(original[i])) {
                    isPadding = false;
                    break;
                }
            }
        }
        if (isPadding) {
            original = Arrays.copyOfRange(original, 0, original.length - pad);
        }

        return original;
    }

    public static byte[] decode(String baseString, Encoder encoder) throws Exception {
        byte[] decodeBytes;
        switch (encoder) {
            case Base64: {
                decodeBytes = Base64.getDecoder().decode(baseString);
                break;
            }
            case Base16: {
                BASE16coder base16Decoder = new BASE16coder();
                decodeBytes = base16Decoder.decode(baseString);
                break;
            }
            case Base32: {
                BASE32coder base32Decoder = new BASE32coder();
                decodeBytes = base32Decoder.decode(baseString);
                break;
            }
            default:// 未知
                decodeBytes = null;
        }
        return decodeBytes;
    }

    /**
     * HMAC-sha1 Note: key - 20byte ； return - 20byte; H( K XOR opad, H(K XOR
     * ipad, text))
     *
     * @throws IllegalStateException
     */
    public static byte[] hmacSha1(String text, byte[] key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException {
        try {
            Mac hmacSha1;
            try {
                hmacSha1 = Mac.getInstance("HmacSHA1");
            } catch (NoSuchAlgorithmException nsae) {
                hmacSha1 = Mac.getInstance("HMAC-SHA-1");
            }
            SecretKeySpec macKey = new SecretKeySpec(key, "RAW");
            hmacSha1.init(macKey);

            return hmacSha1.doFinal(text.getBytes(StandardCharsets.UTF_8));
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }

    /**
     * HMAC-sha256 Note: key - 32byte ；return - 32byte
     *
     * @throws IllegalStateException
     */
    public static byte[] hmacSha256(String text, byte[] key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException {
        try {
            Mac hmacSha256;
            try {
                hmacSha256 = Mac.getInstance("HmacSHA256");
            } catch (NoSuchAlgorithmException nsae) {
                hmacSha256 = Mac.getInstance("HMAC-SHA-256");
            }
            SecretKeySpec macKey = new SecretKeySpec(key, "RAW");
            hmacSha256.init(macKey);
            return hmacSha256.doFinal(text.getBytes(StandardCharsets.UTF_8));
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }

    public static byte[] hmacSha256(byte[] text, byte[] key) {
        try {
            Mac hmacSha256;
            try {
                hmacSha256 = Mac.getInstance("HmacSHA256");
            } catch (NoSuchAlgorithmException nsae) {
                hmacSha256 = Mac.getInstance("HMAC-SHA-256");
            }
            SecretKeySpec macKey = new SecretKeySpec(key, "RAW");
            hmacSha256.init(macKey);
            return hmacSha256.doFinal(text);
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }

    /**
     * MD5 Note: Return - 16byte;
     */
    public static byte[] MD5(String inStr) {
        MessageDigest md = null;
        byte[] digest = null;
        try {
            md = MessageDigest.getInstance("MD5");
            digest = md.digest(inStr.getBytes(StandardCharsets.UTF_8)); // mod by hc 20121115
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return digest;
    }

    /**
     * sha1 Note: Return - 20byte;
     */
    public static byte[] sha1(String inStr) {
        MessageDigest md = null;
        byte[] digest = null;
        try {
            md = MessageDigest.getInstance("SHA-1");
            digest = md.digest(inStr.getBytes(StandardCharsets.UTF_8)); // mod by hc 20121115
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return digest;
    }

    /**
     * sha256 Note: Return - 32byte;
     */
    public static byte[] sha256(String inStr) {
        MessageDigest md = null;
        byte[] digest = null;
        try {
            md = MessageDigest.getInstance("SHA-256");
            digest = md.digest(inStr.getBytes(StandardCharsets.UTF_8)); // mod by hc 20121115
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return digest;
    }

    /**
     * sha256 Note: Return - 32byte;
     */
    public static byte[] sha256Byte(byte[] inByte) {
        MessageDigest md = null;
        byte[] digest = null;
        try {
            md = MessageDigest.getInstance("SHA-256");
            digest = md.digest(inByte);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return digest;
    }


    /**
     * get sm3 hash
     *
     * @throws IOException
     */
    public static byte[] getSM3Hash(byte[] source) throws IOException {
        if (source == null || source.length == 0) {
            return new byte[0];
        }
        byte[] sm3HashValue = SM3.hash(source);

        log.info("hash result -- " + bytesToHexString(sm3HashValue));
        return sm3HashValue;
    }

    /**
     * get sm3 hmac
     *
     * @throws IOException
     */
    public static byte[] getSM3Hmac(byte[] source, byte[] key) throws IOException {
        if (source == null || source.length == 0) {
            return null;
        }
        byte[] sm3HashValue = SM3.hmac_hash(key, source);

        log.info("hash result -- " + bytesToHexString(sm3HashValue));
        return sm3HashValue;
    }

    /**
     * encrypt sms4
     *
     * @throws IOException
     */
    public static byte[] encryptSMS4Nopadding(byte[] in, byte[] key) throws IOException {
        if (in == null || in.length < 16 || key == null || key.length < 16) {
            return null;
        }
        SMS4 sm4 = new SMS4();
        byte[] input = in;
        byte[] out = new byte[input.length];
        int ENCRYPT = 1;
        //加密 128bit
        long starttime = System.nanoTime();
        sm4.sms4(input, input.length, key, out, ENCRYPT);
        return out;
    }

    /**
     * decrypt sms4 nopadding
     *
     * @param in
     * @param key
     * @return
     * @throws IOException
     */
    public static byte[] decryptSMS4NoPadding(byte[] in, byte[] key) throws IOException {
        if (in == null || in.length < 16 || key == null || key.length < 16) {
            return null;
        }

        SMS4 sm4 = new SMS4();
        byte[] out = new byte[in.length];
        int DECRYPT = 0;
        //解密 128bit
        sm4.sms4(in, in.length, key, out, DECRYPT);

        return out;
    }


    /**
     * encrypt sms4
     *
     * @throws IOException
     */
    public static byte[] encryptSMS4PCS5Padding(byte[] in, byte[] key) {
        if (in == null || key == null || key.length < 16) {
            return null;
        }
        SMS4 sm4 = new SMS4();
        byte[] input = sm4.padding_PCS5(in);
        byte[] out = new byte[input.length];
        int ENCRYPT = 1;
        //加密 128bit
        sm4.sms4(input, input.length, key, out, ENCRYPT);
        return out;
    }

    /**
     * decrypt sms4 PCS5padding
     *
     * @param in
     * @param key
     * @return
     * @throws IOException
     */
    public static byte[] decryptSMS4PCS5Padding(byte[] in, byte[] key) {
        if (in == null || in.length < 16 || key == null || key.length < 16) {
            return new byte[0];
        }

        SMS4 sm4 = new SMS4();
        byte[] out = new byte[in.length];
        int DECRYPT = 0;
        //解密 128bit
        sm4.sms4(in, in.length, key, out, DECRYPT);

        return sm4.cutpaddingPCS5(out);
    }


    /**
     * @param src
     * @return HEXstring
     */
    public static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            String hex = Integer.toHexString(src[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            stringBuilder.append(hex.toUpperCase());
        }
        return stringBuilder.toString();
    }

    /**
     * @param src
     * @return byte[]
     */
    public static byte[] hexStrToByte(String src) {
        if (src == null || src.isEmpty()) {
            return new byte[0];
        }
        int len = (src.length() / 2);
        byte[] result = new byte[len];
        char[] achar = src.toCharArray();
        for (int i = 0; i < len; i++) {
            int pos = i * 2;
            result[i] = (byte) (toByte(achar[pos]) << 4 | (toByte(achar[pos + 1])) & 0xff);
        }
        return result;
    }

    private static byte toByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }

    /**
     * 生成SM2的加解密的密钥对
     *
     * @return 包含密钥对的Map，公钥的key为 {@link #PUBLIC_KEY},私钥的key为 {@link #PRIVATE_KEY}
     */
    public static Map<String, String> genSM2KeyPair() {
        final KeyPairOfString sm2Keys = SM2Util.getSm2Keys(false);
        Map<String, String> keyPair = new HashMap<String, String>();
        keyPair.put(PUBLIC_KEY, sm2Keys.getPublicKey());
        keyPair.put(PRIVATE_KEY, sm2Keys.getPrivateKey());
        return keyPair;
    }

    /**
     * 生成Rsa加解密的密钥对
     *
     * @return
     * @throws NoSuchAlgorithmException
     */
    public static Map<String, String> genRSAKeyPair() {

        KeyPairGenerator keyPairGen = null;
        try {
            keyPairGen = KeyPairGenerator.getInstance("RSA");
        } catch (NoSuchAlgorithmException e) {
            log.error("RSA实例化失败", e);
            return new HashMap<>();
        }
        keyPairGen.initialize(1024);//NOSONAR  sonar扫描忽略这段代码
        KeyPair keyPair = keyPairGen.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        Map<String, String> keyMap = new HashMap<String, String>(2);
        keyMap.put(PUBLIC_KEY, Base64.getEncoder().encodeToString(publicKey.getEncoded()));
        keyMap.put(PRIVATE_KEY, Base64.getEncoder().encodeToString(privateKey.getEncoded()));
        return keyMap;
    }

    public static String encode(byte[] baseByte, Encoder encoder) {
        String encodeText;
        switch (encoder) {
            case Base64: {
                encodeText = Base64.getEncoder().encodeToString(baseByte);
                break;
            }
            case Base16: {
                BASE16coder base16Encoder = new BASE16coder();
                encodeText = base16Encoder.encode(baseByte);
                break;
            }
            case Base32: {
                BASE32coder base32Encoder = new BASE32coder();
                encodeText = base32Encoder.encode(baseByte);
                break;
            }
            default:// 未知
                encodeText = null;
        }

        /**
         * 完整的base64定义可见 RFC1421和 RFC2045。编码后的数据比原始数据略长，为原来的4/3。
         * 在电子邮件中，根据RFC822规定，每76个字符，还需要加上一个回车换行。可以估算编码后数据长度大约为原长的135.1%。
         */
        return encodeText != null ? encodeText.replaceAll(System.lineSeparator(), "") : null;
    }

    /**
     * RSA加密
     *
     * @param publicKeyBase64 公钥
     * @param text            待加密字符串,text.getByte("UTF-8")
     * @return 密文
     */
    public static String encryptRSA(String publicKeyBase64, byte[] text) {
        try {
            return Algorithm.encode(RsaUtils.encrypt(text, publicKeyBase64), Encoder.Base64);
        } catch (Exception e) {
            log.error("RSA 加密失败", e);
        }
        return null;
    }

    /**
     * RSA解密
     *
     * @param privateKeyBase64 私钥
     * @param enc              对密文进行base64解码之后的byte[]数据
     * @return 明文
     */
    public static byte[] decryptRSA(String privateKeyBase64, byte[] enc) {
        try {
            return RsaUtils.decrypt(enc, privateKeyBase64);
        } catch (Exception e) {
            log.error("RSA解密失败", e);
        }
        return new byte[0];
    }

    public static byte[] signRSA(String privateKeyBase64, byte[] sourceData) {
        try {
            return RsaUtils.sign(sourceData, privateKeyBase64);
        } catch (Exception e) {
            log.error("签名失败", e);
        }
        return new byte[0];
    }

    public static boolean verifySignRSA(String publicKeyBase64, byte[] sourceData, byte[] signData) {
        try {
            return RsaUtils.verify(sourceData, signData, publicKeyBase64);
        } catch (Exception e) {
            log.error("验证失败", e);
        }
        return false;
    }
}
